package gersemi

import (
	"bytes"
	"encoding/json"
	"errors"
	"fmt"
	"log"
	"net/http"
	"regexp"
	"strings"
	"time"

	"apiote.xyz/p/asgard/idavollr"
	"apiote.xyz/p/asgard/jotunheim"

	"apiote.xyz/p/gott/v2"
	_ "github.com/emersion/go-message/charset"
)

type InvalidMessageError struct{}

func (InvalidMessageError) Error() string {
	return "message does not match withdrawal or deposit regex"
}

type Transaction interface {
	IsTransaction()
}

type TransactionData struct {
	Type        string `json:"type"`
	Date        string `json:"date"` //yyyy-mm-ddT00:00:00+00:00
	Amount      string `json:"amount"`
	Description string `json:"description"`
}

type Withdrawal struct {
	TransactionData
	SourceID        string `json:"source_id"`
	DestinationName string `json:"destination_name"`
}

func (w Withdrawal) IsTransaction() {}

type Deposit struct {
	TransactionData
	SourceName    string `json:"source_name"`
	DestinationID string `json:"destination_id"`
}

func (w Deposit) IsTransaction() {}

type GersemiRequestBody struct {
	Transactions []Transaction `json:"transactions"`
}

type GersemiMailbox struct {
	idavollr.Mailbox
	hc                *http.Client
	withdrawalRegexes []*regexp.Regexp
	depositRegexes    []*regexp.Regexp
}

type GersemiImapMessage struct {
	idavollr.ImapMessage
	hc                *http.Client
	withdrawalRegexes []*regexp.Regexp
	depositRegexes    []*regexp.Regexp
	config            jotunheim.Config
	src, dst          string
	title             string
	amount            string
	day, month, year  string
	requestBody       GersemiRequestBody
	requestBodyBytes  []byte
	request           *http.Request
	response          *http.Response
}

func Gersemi(config jotunheim.Config) error {
	timeout, _ := time.ParseDuration("60s")
	mailbox := &GersemiMailbox{
		Mailbox: idavollr.Mailbox{
			MboxName: config.Gersemi.ImapInbox,
			ImapAdr:  config.Gersemi.ImapAddress,
			ImapUser: config.Gersemi.ImapUsername,
			ImapPass: config.Gersemi.ImapPassword,
			Conf:     config,
		},
		hc: &http.Client{
			Timeout: timeout,
		},
	}
	mailbox.SetupChannels()

	r := gott.R[idavollr.AbstractMailbox]{
		S: mailbox,
	}.
		Bind(prepareRegexes).
		Bind(idavollr.Connect).
		Tee(idavollr.Login).
		Bind(idavollr.SelectInbox).
		Tee(idavollr.CheckEmptyBox).
		Map(idavollr.FetchMessages).
		Tee(createTransactions).
		Tee(idavollr.CheckFetchError).
		Recover(idavollr.IgnoreEmptyBox).
		Recover(idavollr.Disconnect)

	return r.E
}

func prepareRegexes(am idavollr.AbstractMailbox) (idavollr.AbstractMailbox, error) {
	m := am.(*GersemiMailbox)
	for i, wr := range m.Config().Gersemi.WithdrawalRegexes {
		re, err := regexp.Compile(wr)
		if err != nil {
			return m, fmt.Errorf("while compiling withdrawal regex %d: %w", i, err)
		}
		m.withdrawalRegexes = append(m.withdrawalRegexes, re)
	}

	for i, dr := range m.Config().Gersemi.DepositRegexes {
		re, err := regexp.Compile(dr)
		if err != nil {
			return m, fmt.Errorf("while compiling deposit regex %d: %w", i, err)
		}
		m.depositRegexes = append(m.depositRegexes, re)
	}

	return m, nil
}

func createTransactions(am idavollr.AbstractMailbox) error {
	m := am.(*GersemiMailbox)
	for msg := range m.Messages() {
		imapMessage := idavollr.ImapMessage{
			Msg:      msg,
			Sect:     m.Section(),
			Mimetype: m.Config().Gersemi.MessageMime,
		}
		imapMessage.SetClient(m.Client())
		r := gott.R[idavollr.AbstractImapMessage]{
			S: &GersemiImapMessage{
				ImapMessage:       imapMessage,
				config:            m.Config(),
				withdrawalRegexes: m.withdrawalRegexes,
				depositRegexes:    m.depositRegexes,
				hc:                m.hc,
			},
		}.
			Bind(idavollr.ReadMessageBody).
			Bind(idavollr.ParseMimeMessage).
			Bind(idavollr.GetBody).
			Bind(idavollr.ReadBody).
			Bind(createTransaction).
			Bind(marshalBody).
			Bind(createRequest).
			Bind(doRequest).
			Bind(handleHttpError).
			Tee(moveMessage).
			Recover(ignoreInvalidMessage).
			Recover(idavollr.RecoverMalformedMessage).
			Recover(idavollr.RecoverErroredMessages)
		if r.E != nil {
			log.Printf("while processing message %s: %v", msg.Envelope.Subject, r.E)
		}
	}
	return nil
}

func matchRegex(m *GersemiImapMessage, match []string, groupNames []string) *GersemiImapMessage {
	for groupIdx, group := range match {
		names := groupNames[groupIdx]
		for _, name := range strings.Split(names, "_") {
			switch name {
			case "TITLE":
				m.title = regexp.MustCompile(" +").ReplaceAllString(group, " ")
			case "SRC":
				m.src = group
			case "DST":
				m.dst = regexp.MustCompile(" +").ReplaceAllString(group, " ")
			case "AMOUNTC":
				m.amount = group
				m.amount = strings.Replace(m.amount, ",", ".", -1)
			case "AMOUNT":
				m.amount = group
			case "DAY":
				m.day = group
			case "MONTH":
				m.month = group
			case "YEAR":
				m.year = group
			}
		}
	}
	return m
}

func createWithdrawal(m *GersemiImapMessage) *GersemiImapMessage {
	for _, regex := range m.withdrawalRegexes {
		groupNames := regex.SubexpNames()
		matches := regex.FindAllStringSubmatch(string(m.MessageBody()), -1)
		if matches == nil {
			continue
		}
		match := matches[0]
		m = matchRegex(m, match, groupNames)
		transaction := Withdrawal{
			TransactionData: TransactionData{
				Type:        "withdrawal",
				Date:        m.year + "-" + m.month + "-" + m.day + "T06:00:00+00:00",
				Amount:      m.amount,
				Description: m.title,
			},
			SourceID:        m.config.Gersemi.Accounts[m.src],
			DestinationName: m.dst,
		}
		body := GersemiRequestBody{
			Transactions: []Transaction{transaction},
		}
		m.requestBody = body
		return m
	}
	return nil
}

func createDeposit(m *GersemiImapMessage) *GersemiImapMessage {
	for _, regex := range m.depositRegexes {
		groupNames := regex.SubexpNames()
		matches := regex.FindAllStringSubmatch(string(m.MessageBody()), -1)
		if matches == nil {
			continue
		}
		match := matches[0]
		m = matchRegex(m, match, groupNames)
		transaction := Deposit{
			TransactionData: TransactionData{
				Type:        "deposit",
				Date:        m.year + "-" + m.month + "-" + m.day + "T06:00:00+00:00",
				Amount:      m.amount,
				Description: m.title,
			},
			SourceName:    m.src,
			DestinationID: m.config.Gersemi.Accounts[m.dst],
		}
		body := GersemiRequestBody{
			Transactions: []Transaction{transaction},
		}
		m.requestBody = body
		return m
	}
	return nil
}

func createTransaction(am idavollr.AbstractImapMessage) (idavollr.AbstractImapMessage, error) {
	m := am.(*GersemiImapMessage)
	m.src = m.config.Gersemi.DefaultSource
	result := createWithdrawal(m)
	if result != nil {
		return result, nil
	}
	result = createDeposit(m)
	if result != nil {
		return result, nil
	}

	return m, InvalidMessageError{}
}

func marshalBody(am idavollr.AbstractImapMessage) (_ idavollr.AbstractImapMessage, err error) {
	m := am.(*GersemiImapMessage)
	m.requestBodyBytes, err = json.Marshal(m.requestBody)
	return m, err
}

func createRequest(am idavollr.AbstractImapMessage) (_ idavollr.AbstractImapMessage, err error) {
	m := am.(*GersemiImapMessage)
	m.request, err = http.NewRequest("POST", m.config.Gersemi.Firefly+"/api/v1/transactions", bytes.NewReader(m.requestBodyBytes))
	m.request.Header.Add("Authorization", "Bearer "+m.config.Gersemi.FireflyToken)
	m.request.Header.Add("Accept", "application/vnd.api+json")
	m.request.Header.Add("Content-Type", "application/json")
	return m, err
}

func doRequest(am idavollr.AbstractImapMessage) (_ idavollr.AbstractImapMessage, err error) {
	m := am.(*GersemiImapMessage)
	m.response, err = m.hc.Do(m.request)
	return m, err
}

func handleHttpError(am idavollr.AbstractImapMessage) (idavollr.AbstractImapMessage, error) {
	m := am.(*GersemiImapMessage)
	if m.response.StatusCode != 200 {
		return m, fmt.Errorf(m.response.Status)
	}
	return m, nil
}

func moveMessage(am idavollr.AbstractImapMessage) error { // TODO collect messages out of loop and move all
	m := am.(*GersemiImapMessage)
	return idavollr.MoveMsg(m.Client(), m.Msg, m.config.Gersemi.DoneFolder)
}

func ignoreInvalidMessage(s idavollr.AbstractImapMessage, e error) (idavollr.AbstractImapMessage, error) {
	var invalidMessageErr InvalidMessageError
	if errors.As(e, &invalidMessageErr) {
		log.Println(e.Error())
		return s, nil
	}
	return s, e
}
